プログラミング
UITextViewでキーボードがカーソルを隠さないようにする明快な方法
内容霽月です。Swiftちょっと分かる人向けの記事です。
Appleのメモ帳アプリなどでは、改行しまくってキーボードに迫ったとしても、キーボードの下にカーソルが隠れることなく、自動的にスクロールされることで、カーソルが常に見える位置に存在するようになっていますよね。
その機能を再現する方法についてネットで少し調べてみましたが、あまり良い解決策は見つかりませんでした。ということで、この記事では、単純で分かりやすくその機能を実現する方法について紹介します。
環境macOS Ventura 13.2.1シミュレータ: iPhone 14 Pro(iOS16.2)Xcode14.2Swift5.7.2
UITextViewを配置まずはUITextViewを画面に配置します。import UIKit
class ViewController: UIViewController {
    private let textView = UITextView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(textView)
        textView.frame = view.bounds
        textView.text = String(repeating: "これは UITextView を使ったテストです。\n", count: 100)
    }
}実行すると適当な文章が100行表示されます。
キーボードの高さを取得次に、キーボードが開かれた時に、キーボードの高さを取得するようにします。
保存先の変数を作って、    private var keyboardHeight: Double = 0.0以下をviewDidLoadの中に書いて、キーボードが表示された時にメソッドが呼び出されるように登録し、        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillShow(_:)),
            name: UIResponder.keyboardWillShowNotification,
            object: nil)キーボードの高さを取得する処理を書きます。    @objc func keyboardWillShow(_ notification: Notification) {
        guard let userInfo = notification.userInfo else { return }
        guard let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
        keyboardHeight = keyboardFrame.height
    }実行してエラーがないことを確認して、次の段階に進みます。
textViewDidChange関数textViewに変更が加えられた時に呼び出される関数を作ります。
textViewのdelegateをselfにしておいて、        textView.delegate = selftextViewDidChange関数の中に書いていきます。extension ViewController: UITextViewDelegate {
    func textViewDidChange(_ textView: UITextView) {
        
    }
}
目的の機能を作成必要な情報は「現在のスクロール具合」「カーソルがある矩形領域」「キーボードが開いてても見える矩形領域」です。この3つの情報を取得します。        var contentOffset = textView.contentOffset
        let cursorPosition = textView.selectedTextRange?.start ?? textView.beginningOfDocument
        var cursorRect = textView.caretRect(for: cursorPosition)
        let visibleRect = CGRect(x: 0, y: 0, width: textView.bounds.width, height: textView.bounds.height - keyboardHeight)
        cursorRect.origin.y -= contentOffset.y順に「contentOffset」「cursorRect」「visibleRect」という名前で取得しました。
カーソルの下端がvisibleRectの下端より下に行かないようにします。        if cursorRect.maxY > visibleRect.maxY {
            contentOffset.y += cursorRect.maxY - visibleRect.maxY
        }最後にcontentOffsetの変更を保存して終わりです。        textView.setContentOffset(contentOffset, animated: true)
実行して確認問題なく実行できました。カーソルが見えない場所に行ってしまう現象は解決され、とても便利なUITextViewの完成です!
と、思いきや?
textVIewのコンテンツの下端に余白がないために、下の方を編集しようとすると物凄くやりにくいことになってしまいました。
キーボードが開いている時は下端にキーボードの高さ分の余白を開けるようにしてみましょう。
keyboardWillShowに以下を追加します。        textView.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight, right: 0)
        textViewDidChange(textView)キーボードが閉じた時の通知も登録して、        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillHide(_:)),
            name: UIResponder.keyboardWillHideNotification,
            object: nil)キーボードが閉じた時に余白を元に戻す処理を書きます。    @objc func keyboardWillHide(_ notification: Notification) {
        textView.textContainerInset = UIEdgeInsets.zero
    }これで完璧です。
最終的なコードの全文import UIKit
class ViewController: UIViewController {
    private let textView = UITextView()
    
    private var keyboardHeight: Double = 0.0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(textView)
        textView.frame = view.bounds
        textView.delegate = self
        textView.text = String(repeating: "これは UITextView を使ったテストです。\n", count: 100)
        
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillShow(_:)),
            name: UIResponder.keyboardWillShowNotification,
            object: nil)
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillHide(_:)),
            name: UIResponder.keyboardWillHideNotification,
            object: nil)
    }
    
    @objc func keyboardWillShow(_ notification: Notification) {
        guard let userInfo = notification.userInfo else { return }
        guard let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
        keyboardHeight = keyboardFrame.height
        
        textView.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight, right: 0)
        textViewDidChange(textView)
    }
    
    @objc func keyboardWillHide(_ notification: Notification) {
        textView.textContainerInset = UIEdgeInsets.zero
    }
}
extension ViewController: UITextViewDelegate {
    func textViewDidChange(_ textView: UITextView) {
        var contentOffset = textView.contentOffset
        let cursorPosition = textView.selectedTextRange?.start ?? textView.beginningOfDocument
        var cursorRect = textView.caretRect(for: cursorPosition)
        let visibleRect = CGRect(x: 0, y: 0, width: textView.bounds.width, height: textView.bounds.height - keyboardHeight)
        cursorRect.origin.y -= contentOffset.y
        if cursorRect.maxY > visibleRect.maxY {
            contentOffset.y += cursorRect.maxY - visibleRect.maxY
        }
        textView.setContentOffset(contentOffset, animated: true)
    }
}
内容を見る
プログラミング
自作AIをAndroidにのっける
動機近年、人工知能を利用したサービスが増えている。
例えばChat GPT。言語処理をしてくれるAIだが非常に精度がいい。APIも公開されており、気軽にサービスに組み込むことができる。他にも様々なAIが公開されており、人工知能を用いたサービスというのは珍しく無くなってきた。
しかし、提供されたAPIを叩いただけでは人工知能を使った感が薄く感じる。
自分でAIを用意してサービスに組み込んでみたいと思う人は少なくないはず。
自分でサーバーを用意してAPIを公開してもいいが、サーバーの維持費、通信費用などがかかってしまう。端末上で動かすことができればこの問題も解決できる。
そう思い探してみたところ、「Tensorflow Lite」に行き着いた。そこで、実際にこのライブラリを用いてAndroidアプリを作ってみたい。
Tensorflow Liteとは機械学習を行うためのライブラリとして「Tensorflow」というものがある。比較的自由に機械学習を行えるため世界中で愛されているが、モバイル端末上で実行するには少し重たすぎる。
そこでTensorflow Liteである。このライブラリを使うことでモバイル端末上(iOS、Android、Raspberry pi等)で利用できるくらいに軽量化される。
同じようにモバイル端末上で人工知能を利用するためのライブラリとして「ML Kit」というものも存在するが、これだと自分で作成したモデルを利用できない。その点ではこちらの方が(手間はかかるが)自由度が高い。
使い方下の方で小難しく書いているが使い方はものすごく簡単である。
1. tfliteモデルを用意するtfliteとはTensorflow Lite専用の型。
自分で作ってもいいし、ネットから拾ってきてもいい。ここでみんな配布してるのですぐ手に入る。
2. Androidにぶち込むimport機能を使って指定の場所にtfliteモデルを入れる。
3. 使う入れればAndroid側が勝手にモデル用のクラスを作ってくれる。だからその関数を叩けばいい
例えば以下のコード。
mnistModel.process(target.toTensorBuffer()) // これを叩くだけで識別結果が返ってくる。
このmnistModelというのが用意されたモデル用クラス(のインスタンス)。ここに生えているprocess関数に入力値を渡してやれば出力が得られる。
これだけで完成。
ものすごく簡単にできることがわかると思う。
アプリ作成実際にAndroidアプリを作ってみる。
作ったアプリは以下の通り。
数字を手書きしてボタンを押せば人工知能で識別してくれるアプリ。
返送速度もいい感じ(これはエミュレーターだが、実機でやればもう少し早くなる)。
こんな感じのアプリを作成していく
1.モデルの作成まずは人工知能のモデルづくりから始める。
Tensorflow Liteでは.tfliteという独自の拡張子のモデルを用意する必要がある。これには以下の方法が存在する。
1. Tensorflow Hubから有志が作成したモデルを取得する
2. TFLite Model Makerを使って事前学習済みのモデルを学習させて取得する
3. Tensorflowでモデルを作成してそれをtflite形式に変換する
今回は3の方式で行う。
ここからはPythonで記述する。
まずはTensorflowでモデルを作成する。
 # モデル構造の設定
 model = tf.keras.models.Sequential([
  tf.keras.layers.Conv2D(48, kernel_size = (4, 4), activation="relu", input_shape=(28,28,1)),
  tf.keras.layers.BatchNormalization(),
  tf.keras.layers.Conv2D(96, kernel_size = (4, 4), activation="relu"),
  tf.keras.layers.BatchNormalization(),
  tf.keras.layers.Flatten(),
  tf.keras.layers.Dense(200),
  tf.keras.layers.BatchNormalization(),
  tf.keras.layers.Dense(10),
 ])
 
 loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
 model.compile(optimizer='adam',
        loss=loss_fn,
        metrics=['accuracy'])
 model.fit(train_input, train_label, epochs=5)
このモデルにMnistのデータセットを入れて学習させたところ、識別率98.84%になった。
これを一度SavedModel形式に変換する。
 model.save("mnist_model")
そこからconverterを用いてtflite形式に変換する
 converter = tf.lite.TFLiteConverter.from_saved_model("mnist_model")
 tflite_model = converter.convert()
 with open("mnist_model.tflite", 'wb') as o_:
   o_.write(tflite_model)
tfliteに変換することで、モバイル端末でも問題なく動作するように軽量化が行われる。
ただし、軽量化する中で精度が低下することがある。
そこで、以下のコードで精度を計測する。
# tflite形式のモデルのインスタンス化
interpreter = tf.lite.Interpreter("MnistModel.tflite")
interpreter.allocate_tensors()
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()
# 識別結果が正しい個数
correct_num = 0
for i in range(len(test_input)):
# テストデータを入力の型に合わせる
input = test_input[i].reshape(input_details[0]['shape']).astype(np.float32)
interpreter.set_tensor(input_details[0]['index'], input)
interpreter.invoke()
preds_tf_lite = interpreter.get_tensor(output_details[0]['index'])
# 正答率を計算
correct_num += np.argmax(preds_tf_lite) == test_label[i]
print(f"accuracy: {correct_num / len(test_input)}")
識別率は98.84%で、低下は見受けられなかった。
こうしてモデルを用意できた。
詳細なコードは以下に示す。
https://colab.research.google.com/drive/18J0DxrKAMGnMvu0C2AYZlqxREkglFfNS?usp=sharing
2. アプリ部分の作成次にアプリの部分を作成する。
様々な記事を見てみると、識別部分まで全てActivityに記述しているものが散見された。
お試しで使う分にはこれで十分ではあるが、今回は普通にアプリに組み込んでみて問題なく使えるかをみてみたい。
そこで以下のアーキテクチャに沿って開発をした。
TFLiteModelは技術的要素が多く、バックエンドよりの処理であるためRepository層に押し込んでいる。
また、Bitmap型はAndroid独特の型である。今回はビジネスロジックが特定の技術に依存するのもどうかと思い一度Imageに変換している。ただし、その場合TensorBufferやTensorImageへの変換に苦労することになる。
TFLiteではBitmap -> TensorImageへの変換関数も用意してくれているため、Bitmapに依存するように書いても十分問題ないと思われる。
後は(TFLiteModel以外の部分を)この仕様書に合わせて頑張って実装する。
3. モデルとアプリの接続まずライブラリの依存関係を記述する。
version catalogに以下を記述。
 # Tensorflow Lite
 tensorflow-lite = { group = "org.tensorflow", name = "tensorflow-lite", version.ref = "tensorflow-lite" }
 tensorflow-lite-support = { group = "org.tensorflow", name = "tensorflow-lite-support", version.ref = "tensorflow-lite-support" }
 tensorflow-lite-metadata = { group = "org.tensorflow", name = "tensorflow-lite-metadata", version.ref = "tensorflow-lite-support" }
tensorflow-lite-gpuを導入することでGPUを使用することもできるようになるが、今回は省いている。
次にModelをAndroidのライブラリに入れていく。
以下の画像のように選択。
クリックすると以下のようなモーダルが出てくるため記述。
するとmlファイルが出現し、ここに格納される。
こちらのファイルを見ると、使い方が書かれている。
こちらに従ってコードを記述していく。
まずはModelのインスタンスの作成。
Dagger Hiltを使ってDIできるようにしておく。
 @Module
 @InstallIn(SingletonComponent::class)
 class ModelProvider {
   @Provides
   fun providesMnistModel(
     @ApplicationContext context: Context,
   ): MnistModel = MnistModel.newInstance(context)
 }
その後、Repositoryに以下を記述する。    override suspend fun classify(
        target: Image,
    ): ApiResult<ClassifyResult, DomainException> = withContext(dispatcher) {
        mnistModel
            .process(target.toTensorBuffer())
            .outputFeature0AsTensorBuffer
            .floatArray
            .let { buffer ->
                ClassifyResult(
                    zero = buffer[0],
                    one = buffer[1],
                    two = buffer[2],
                    three = buffer[3],
                    four = buffer[4],
                    five = buffer[5],
                    six = buffer[6],
                    seven = buffer[7],
                    eight = buffer[8],
                    nine = buffer[9],
                )
            }.let { ApiResult.Success(it) }
    }
    
    private fun Image.toTensorBuffer() = TensorBuffer
        .createFixedSize(intArrayOf(1, 28, 28), DataType.FLOAT32)
        .also { buffer -> buffer.loadArray(this.pixels.toFloatArray()) }
ぱっと見色々書いているように見えるが、識別自体はprocess関数を叩くだけでできており、非常に手軽である。
mnistModel.process(target.toTensorBuffer()) // これを叩くだけで識別結果が返ってくる。
ただデータの変換だけめんどくさい。実際残りのコードは全てデータの変換である。
Modelにmetadataを付与することでここら辺の記述はもう少し簡単にできるが、今回はこのまま行った。
以上で完成。結構簡単にできる。
完成したコードはこちら
https://github.com/Wansuko-cmd/mnist-application
感想モデル作るのはそこまで難しくない。最悪既存やつ使えばいいアプリとの接続は面倒。入力されたものを頑張ってBufferに変換していく必要がある。(これはTensorImageを使えば解決できそう)モデルがでかい。今回のモデルサイズは37.5MB。一応Firebaseと連携させることで初期インストールしなくてもよくなるが、それでもでかい自分の作ったモデルをアプリに載せるのは結構楽しい気になった人は是非Androidを使って組んでみてほしい。公式チュートリアルに従ってやれば一日でできるようになる。
内容を見る
プログラミング
懐かしの高校物理をPythonで解こう ~ただし空気抵抗は考えるものとする~
はじめにこんにちは、ごまです!
つい最近、Python で微分方程式を解いたり、グラフを描画することを可能にするライブラリがあることを知りました。
自分の知る限りでは、微分方程式を解くためには SymPy というライブラリが有効なようなのですが、正直なところあまり使ったことがありません。
そのため、SymPyの使い方を習得するためにも、今回はこのライブラリと Python を用いて、高校物理の問題を、それも (高校物理では殆どの場合考慮されなかった) 空気抵抗の考慮をしつつ解いてみようと思います。環境Windows 11Python 3.9.7SymPy 1.11.1SymPyについて軽く説明と実演本格的に演習に入る前に、SymPyについての軽い説明と、いくつかの実演を行います。
今回使用するライブラリであるSymPyは、簡単に言うと数式の処理や計算を行えるPythonのライブラリです。
これを用いることで、Pythonを用いての微分積分や因数分解などの様々な数式の処理が可能になります。
前置きはここまでとして、実際にSymPyを使ってみます。本稿ではこれ以降、SymPyをsymとしてimportします。import sympy as sym
因数分解まずは因数分解をやってみましょう。
SymPyで$x$や $y$ などの変数を扱う際は、次のようにsympy.symbols()を用いて定義を行います。x,y = sym.symbols('x,y')
もちろん1文字ずつ定義を行うことも可能です。
因数分解はsympy.factor()の引数として、因数分解したい数式を渡すことによって行えます。
今回は $2x^{4}-9x^{3}-x^{2}-9x+2$という数式を因数分解してみます。
具体的には次のように記述します。sym.factor(2*x**4 -9*x**3 -x**2 -9*x +2)
少し記法が分かりづらいですが、この場合の*(アスタリスク)一つは乗算演算子、二つはべき乗演算子です。
例えば $a \times b$ はa*b、 $a^{b}$ はa**bと表現されます。
因数分解の結果は次のようになりました。
どうやら無事に因数分解できたようです。
これも記法が分かりづらいですが、SymPy はsympy.latex() を用いることで、数式を LaTeX の記法で出力してくれます。
そのため次のような関数 output() を定義することで、以降の結果を LaTeX の記法で出力することとします。def output(ans):
    print("\n",sym.latex(ans),"\n")
実際に先程の計算を引数にして output() を実行すると次のような出力が得られました。
したがって結果は $ \left(x^{2} - 5 x + 1\right) \left(2 x^{2} + x + 2\right) $ となり、やはり問題なく因数分解できていることがわかります。
微分積分微分積分についても扱っておきます。
まず微分ですが、sympy.diff()を用いることで導関数を求めることができます。
例えば、 $x^{3}$ の $x$ に関しての1階微分は次のように書くことができ、結果として $3x^{2}$ が出力されます。output(sym.diff(x**3,x))
$n$ 回微分をしたければ、 $n$ の数だけ変数を渡すか、変数のあとに $n$ を渡すことによって行えます。
例えば、 $x^{3}$ の $x$ に関しての2階微分は次のように書くことができ、結果としてどちらも $6x$ が出力されます。output(sym.diff(x**3,x,x))
output(sym.diff(x**3,x,2))
次に積分について説明します。不定積分を行う際はsym.integrate()関数に式と変数を与えればよいです。
例えば、 $6x$ を $x$ について不定積分しようとする場合は次のように書くことができ、結果として $3x^{2}$ が出力されます。output(sym.integrate(6*x))
積分定数が含まれていませんが、これは微分方程式を解く場合には問題ありません。
定積分については、定積分の下限と上限を与えることで計算が行なえます。
次の計算では結果として48を得ます。output(sym.integrate(6*x,(x,3,5)))
微分方程式今回の記事で最も重要な、微分方程式の計算について説明します。
まず次のように、変数 $x$ に加えて、未定義の関数$f$を宣言します。
関数の宣言はsympy.Function()で行います。x = sym.symbols('x')
f = sym.Function('f')
そして、関数 $f(x)$ についての微分方程式を次のように宣言します。
今回解く微分方程式は
$$\frac{d^{2}}{dx^{2}}f(x)-\frac{d}{dx}f(x)-2f(x)=0$$
です。
eq = sym.Eq(f(x).diff(x, x) - f(x).diff(x) - 2*f(x), 0)
方程式の定義はsympy.Eq()で行います。第一引数は方程式の左辺、第二引数は方程式の右辺です。
右辺は今回は0に設定していますが、勿論他の引数を渡すこともできます( $e^x$ とか)。
そして、微分方程式はsympy.dsolve()関数に先程宣言した方程式eqを引数として渡すことにより解くことができます。output(sym.dsolve(eq, f(x)))
その結果、
$$f{\left(x \right)} = C{1} e^{- x} + C{2} e^{2 x} $$
が出力されました。
今回は不定積分のときと異なり、適切に任意定数 $C{1},C{2}$ が用いられています。
本稿ではこれを用いて、運動方程式の解を求めていきます。
今回扱う問題今回は河合出版の「物理のエッセンス 四訂版 力学・波動」の中から、簡単な力学の問題を抜粋して解いていきます。
比較のため、それぞれの問題に対して「空気抵抗ナシ」「空気抵抗アリ」の両方のパターンの解答の流れを示します。
演習放物運動問題質量$m$の質点を床から初速$v_{0}$で角度$\theta$の方向に投げた場合,最高点に達するまでの時間$t_{1}$と最高点の高さ$h$を求めよ。水平到達距離$x$を求めよ。解答(空気抵抗ナシ)1. 水平方向に $x$ 軸、鉛直方向に $y$ 軸を取ることを考えると、鉛直方向の運動方程式は
$$m\frac{d^{2}y}{dt^{2}}=-mg$$
となります。
両辺を $m$ で割り $g$ を移項すると
$$\frac{d^{2}y}{dt^{2}}+g=0$$
となるため、これをSymPyで記述するとeq1_0 = sym.Eq(y(t).diff(t, 2)+g)
これを微分方程式としてsympy.dsolve()を用いて解く際、eq1_2 = sym.dsolve(eq1_0, y(t))
と記述することができ、この解は
$$
  y{\left(t \right)} = C_{1} + C_{2} t - \frac{g t^{2}}{2}
$$
となリます。
実は、SymPyでは微分方程式を解く際に初期条件を課すことができ、ics={}の形で記述できます。
今回、 $y(0)=0$ 、 $\frac{dy}{dt}(0)=v_{0}\sin\theta$ ですから、先程の式はeq1_2 = sym.dsolve(eq1_0, y(t), ics={y(0): 0, y(t).diff(t, 1).subs(t, 0): v0*sym.sin(theta)})
と書き直すことができます。subs()関数は代入を行っていると考えていただければ大丈夫です。
$y(t)$ の1階微分に $t=0$ を代入したとき、その値が $v_{0}\sin\theta$ であるということを表しています( $v_{0}$ と $\theta$ は新たにsympy.symbolsで定義しました)。
そうして改めて微分方程式を解くと、解として
$$
  y{\left(t \right)} = - \frac{g t^{2}}{2} + t v_{0} \sin{\left(\theta \right)}
$$
が得られます。
したがって、これの両辺を $t$ について微分した式は
$$
\frac{dy}{dt}=-gt+v_{0}\sin\theta
$$
ですから、 $t=t_{1}$ のとき、すなわち最高点における $y$ 方向の速さが0であることから、この式の左辺を0とおいた式を$t$ について次のように解きます。t_1 = sym.solve(eq1_1, t)
これによって、解が次のように出力されます。
したがって、
$$
  t_{1}= \left[ \frac{v_{0} \sin{\left(\theta \right)}}{g}\right]
$$
ということがわかります。
最高点 $h$ を求めるには、この $t_1$ を(4)式に代入すればいいわけですから、h = eq1_2.subs(t, ((v0*sym.sin(theta)/g)))
このように記述してやることで、値として
$$
  y{\left(\frac{v_{0} \sin{\left(\theta \right)}}{g} \right)} = \frac{v_{0}^{2} \sin^{2}{\left(\theta \right)}}{2 g}
$$
が得られます。これが$h$ですから、書き直すと
$$
h = \frac{v_{0}^{2} \sin^{2}{\left(\theta \right)}}{2 g}
$$
となり、少々不格好ですが無事に小問1が解けたことになります。
2.投げ出されてから落下するまでの時間を $t_{2}$ とすると$t=t_{2}$のとき$y=0$ですから、
t_2 = sym.solve(eq1_2.subs(y(t),0), t)
と記述してやることで、解として
$$
\left[ 0, \ \frac{2 v_{0} \sin{\left(\theta \right)}}{g}\right]
$$
が得られます。$t_{2}\neq0$ですから、
$$
t_{2}=\frac{2 v_{0} \sin{\left(\theta \right)}}{g}(=2t_{1})
$$
であることは明らかです。
ここで、質点に対しては水平方向の力は何も加わっていませんから、水平方向の運動方程式は次のようになります。
$$
  m\frac{d^{2}x}{dt^{2}}=0
$$
したがって両辺を $m$ で割ったものをこのように記述します。eq2_0=sym.Eq(x(t).diff(t, 2))
これを、微分方程式として、初期条件 $x(0)=0$ 、 $\frac{d}{dt}x(t)=v_{0}\cos\theta$ を課して解くには次のように記述すれば良く、eq2_2 = sym.dsolve(eq2_0, x(t), ics={x(0): 0, x(t).diff(t, 1).subs(t, 0): v0*sym.cos(theta)})
解として
$$
x{\left(t \right)} = t v_{0} \cos{\left(\theta \right)}
$$
が得られます。
これに $t=t_{2}$ を代入して解くためにはx_l= eq2_2.subs(t, ((2*v0*sym.sin(theta)/g)))
と記述でき、解として
$$
  x{\left(\frac{2 v_{0} \sin{\left(\theta \right)}}{g} \right)} = \frac{2 v_{0}^{2} \sin{\left(\theta \right)} \cos{\left(\theta \right)}}{g}
$$
すなわち水平到達距離 $x$ として
$$
  x= \frac{2 v_{0}^{2} \sin{\left(\theta \right)} \cos{\left(\theta \right)}}{g}
$$
が得られます。解答(空気抵抗アリ)1. $x$ 軸および $y$ 軸を空気抵抗ナシの場合と同様に取ることを考えます。
ここで、空気抵抗の比例定数を $\kappa$ (カッパ)とすると、 $y$ 軸方向の運動方程式は次のようになります。
$$
  m\frac{d^{2}y}{dt^{2}}=-mg-\kappa\frac{dy}{dt}
$$
両辺を $m$ で割り移項すると
$$
  \frac{d^{2}y}{dt^{2}}+\frac{\kappa}{m}\frac{dy}{dt}+g=0
$$
となります。これを記述するとeq1_0 = sym.Eq(y(t).diff(t, 2)+(kappa/m)*y(t).diff(t, 1)+g,0)
と書けますから、これを次のようにsympy.dsolve()を用いて、初期条件 $x(0)=0$ 、 $\frac{d}{dt}x(t)=v_{0}\cos\theta$ を課して解きます。eq1_2 = sym.dsolve(eq1_0, y(t), ics={y(0): 0, y(t).diff(t, 1).subs(t, 0): v0*sym.sin(theta)})
すると次のような解が得られます。
$$
  y{\left(t \right)} = - \frac{g m t}{\kappa} + \frac{\left(- g m^{2} - \kappa m v_{0} \sin{\left(\theta \right)}\right) e^{- \frac{\kappa t}{m}}}{\kappa^{2}} + \frac{g m^{2} + \kappa m v_{0} \sin{\left(\theta \right)}}{\kappa^{2}}
$$
空気抵抗ナシの場合に比べて非常に式が複雑になっていますが、解法は特に変わりません。
式自体が長くなったので省略しましたが、式の両辺を微分すると
$$
\frac{d}{dt}y(t) = -\frac{g m}{\kappa} - \frac{\left(- g m^{2} - \kappa m v_{0} \sin{\left(\theta \right)}\right) e^{- \frac{\kappa t}{m}}}{\kappa m}
$$
となります。
あとは空気抵抗ナシの場合と同様に、 $t=t_{1} $のとき $dy/dt=0$ となると考えて、直前の式を用いて定義したeq1_1を $t$ について解くと、
sympy.solve()を用いて次のように書くことができ、t_1 = sym.solve(eq1_1, t)
解として
$$
  t_{1}= \left[ \frac{m \log{\left(1 + \frac{\kappa v_{0} \sin{\left(\theta \right)}}{g m} \right)}}{\kappa}\right]
$$
が出力されます。最高点$h$も空気抵抗アリの場合と同様にして、 $t_1$ を $y(t)$ の式に代入すれば良く、h = eq1_2.subs(t, m*sym.log(1 + kappa*v0*sym.sin(theta)/(g*m))/kappa)
と記述すれば、計算結果として
$$
y{\left(\frac{m \log{\left(1 + \frac{\kappa v_{0} \sin{\left(\theta \right)}}{g m} \right)}}{\kappa} \right)} = - \frac{g m^{2} \log{\left(1 + \frac{\kappa v_{0} \sin{\left(\theta \right)}}{g m} \right)}}{\kappa^{2}} + \frac{g m^{2} + \kappa m v_{0} \sin{\left(\theta \right)}}{\kappa^{2}} \\+ \frac{- g m^{2} - \kappa m v_{0} \sin{\left(\theta \right)}}{\kappa^{2} \cdot \left(1 + \frac{\kappa v_{0} \sin{\left(\theta \right)}}{g m}\right)}
$$
が得られますから、
$$
h= - \frac{g m^{2} \log{\left(1 + \frac{\kappa v{0} \sin{\left(\theta \right)}}{g m} \right)}}{\kappa^{2}} + \frac{g m^{2} + \kappa m v{0} \sin{\left(\theta \right)}}{\kappa^{2}} + \frac{- g m^{2} - \kappa m v_{0} \sin{\left(\theta \right)}}{\kappa^{2} \cdot \left(1 + \frac{\kappa v_{0} \sin{\left(\theta \right)}}{g m}\right)}
$$
とわかります。
2.こちらの問題も空気抵抗アリの場合と同様に解く、といいたいところですが、数式が複雑化してしまったせいか、
投げ上げてから落下するまでの時間を求める過程でプログラムが停止してしまい計算を完了することができませんでした。
そのため、残念ですがこのあたりは今後の課題とします。
余談sympy.plot()を用いれば、物体が実際にどのような運動をするのか、というグラフをプロットすることができます。
次の図は私が適当に質点の質量やら投げる角度やら空気抵抗定数やらを指定してプロットした結果です。青が真空中での $y-t$ グラフ、オレンジが流体中での $y-t$ グラフです。
(ここで流体と表現したのは、空気抵抗定数あたりの計算を厳密にしなかったせいで現実ではありえない空気抵抗が発生している可能性があるからです。下手したらタイトル詐欺かも...)
終わりに至らない点も多くありましたが、SymPyを用いた計算の方法を習得することができ、個人的には有意義な取り組みだったと思います。
もしかしたらまた同様の記事を書くかもしれません。ここまでご覧いただきありがとうございました。
参考文献maskot1977 . "微分や微分方程式をPythonで理解する" . "Qiita" . 2019-01-09 .https://qiita.com/maskot1977/items/b4395da5f33f70cd4a09, (参照 2023-02-20)大窪 貴洋. "Sympyによる代数計算" . "コンピューター処理 ドキュメント" . 不明. https://amorphous.tf.chiba-.jp/lecture.files/chemcomputer/15Sympy%E3%81%AB%E3%82%88%E3%82%8B%E4%BB%A3%E6%95%B0%E8%A8%88%E7%AE%97/15.html,(参照 2023-02-27).SymPy Development Team. "SymPy". 2021. https://www.sympy.org/en/index.html, (参照 2023-02-28).浜島清利, 物理のエッセンス四訂版 力学・波動, 河合出版, 2013
内容を見る
プログラミング
TwitterにDeflate圧縮されたバイナリをアップロードして利用する 🤔
まずはこれらのツイートを見てほしい。echo 'class ${public static void main(String[]a)throws Exception{var b=new int[810000];https://t.co/hd3ce1Xpzc(new https://t.co/vgGGZeOChL.File("/media/0")).getRGB(0,0,900,900,b,0,900);for(int i:b)if((i&255)!=0)System.out.write(i);}}'>$.java
java $.*>_.java
java _.* #シェル芸 pic.twitter.com/aCxDjD9ZS1— 社畜ちゃん (@Shachiku_nyan) February 23, 2023
次は◉のターンめう~
12345678
A
B
C
D 〇◉
E ◉〇
F
G
H https://t.co/TZGGgxwtH7— シェル芸bot (@minyoruminyon) February 23, 2023
5F6F6E4F5G6G7G8H3D5H8G3E2E6D6H7H7D4G7E3F2G7F8F8E4H4C5C3C5B2C1C6B7A8C8D7C2D6C7B3G3H2H8B4B5A8A6A1H3A4A1B1A3B2B1E2F1G1F1D2A #シェル芸 https://t.co/mYeyPWMN9Y— 社畜ちゃん (@Shachiku_nyan) February 23, 2023
〇の勝ちめう!
12345678
A◉〇〇〇〇〇〇〇
B◉〇〇〇〇〇〇〇
C◉〇〇〇〇〇〇〇
D〇〇〇〇〇〇〇〇
E〇〇〇〇〇〇〇〇
F〇〇〇〇〇〇〇〇
G〇〇〇〇〇〇〇〇
H〇〇〇〇〇〇〇〇 https://t.co/1Ux236CqEU— シェル芸bot (@minyoruminyon) February 23, 2023
シェル芸bot(@minyoruminyon)とは、それがフォローしているユーザが #シェル芸 等のハッシュタグをつけてシェルコマンドをツイートしたとき、引用RTでその出力をつぶやくbot [注1] 。
つまり、上記のツイートはシェル芸botにjavaコマンドを実行させてオセロをしているということになる。しかしオセロのソースコードは含まれていないように見えるが……?
/media/0シェル芸botには、ツイートに添付された画像を /media ディレクトリ下にあるものとして実行する機能がある。1枚ならその画像は /media/0 。
つまり最初のツイートは、画像を読み込んで java.awt.image.BufferedImage.getRGB(int startX, int startY, int w, int h, int[] rgbArray, int offset, int scansize) によって埋め込まれたバイナリを取り出しているということになる。
記事タイトルが物々しかったが、「Deflate圧縮されたバイナリ」とは、ただ単に「バイナリを埋め込んだPNG画像」のこと。
ちなみにDeflateはおなじみZIPやgzipにも使われる可逆圧縮アルゴリズム。
ちなみに元ツイートを書いた人によると、埋め込まれているのはJavaのソースらしい [2]。手元で実行してみたらわかる。
扱える画像今どうなっているかは知らないが、どうも数年前時点でのTwitterの画像の仕様はこうらしい [3](読み間違いがあれば申し訳ない)。JPEGは原則、quality85の4:2:0クロマサブサンプリングされたJPEGとして再エンコードされる。ただし次の条件をすべて満たすならば再エンコードしない。画像サイズが4096x4096以下である。5MB以下である。画像の回転が必要であるような情報を持っていない。1ピクセルあたり1バイト未満である。WebPもquality85のJPEGに変換される。PNGは色深度や画像サイズによって異なる。PNG-8は保持される。PNG-24,PNG-32のうち、実際の色数が256色以下であるならばPNG-8に変換される。上記を満たさないPNG-24,PNG-32のうち、画像サイズが900x900以下ならば保持される。上記を満たさないPNG画像は、JPEGに変換される。扱える最大の画像サイズは4096x4096である。
つまり、900x900のPNG-32を使えば約3MBもの情報を使うことができる。4枚使えば4倍(要検証)。
……4096x4096のPNG-8でも16MBの情報を使えるのでは?などと思ったが多分JPEGに変換されるんだろう。知らんけど。
あとはプロトコルを交換しておけばメモ帳アプリのスクショをツイートするよりも大量の情報を一気に投稿できる(誰にわかるねん)。
おまけ失敗しとるやないかい!
Lorem ipsumが出る予定だったのに/media/0: PNG image data, 15 x 10, 8-bit colormap, non-interlaced https://t.co/V6BDW0Wr5q— シェル芸bot (@minyoruminyon) February 28, 2023
ツイート1: 本当なら 8-bit/color RGB になってもらう予定だったが、256色以下だったので colormap に変換されてしまった例!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`a%bcdefghijklllllllll https://t.co/MTbx2B4GR7— シェル芸bot (@minyoruminyon) February 28, 2023
ツイート2: WebPに変換される(多分?)とかいう知らない仕様によってASCIIコード表から順番に持ってきたみたいな謎文字列が出ている例
参考文献[1] 社畜ちゃん(@Shachiku_nyan)「echo 'class ${public static void …」 https://twitter.com/Shachiku_nyan/status/1628601778345500674 (2023年2月27日アクセス)
[2] 社畜ちゃん(@Shachiku_nyan)「@AAAR_Salmon javaのソースそのまま押し込めてますw」 https://twitter.com/Shachiku_nyan/status/1628699291454963712 (2023年2月28日アクセス)
[3] NolanOBrien「Upcoming changes to PNG image support」 https://twittercommunity.com/t/upcoming-changes-to-png-image-support/118695 (2023年2月28日アクセス)
[注1] ふるつき「今の所判明しているシェル芸botの仕様」 https://furutsuki.hatenablog.com/entry/2018/07/13/221806 (2023年2月28日アクセス)
内容を見る